import { BuildingConfig } from './buildingConfig';

export class BuildingHelper {
    /** 地形 */
    Terrain: any = {};
    Grid: any = {};
    Encoded: string = '';
    squareX: number = 0;
    squareY: number = 0;
    minBoundX: number = 0;
    minBoundY: number = 0;
    AllTrees;

    Dummies = {};
    Players: any = {};
    constructor() {
        this.__InitBuildingHelper();
    }

    private __InitBuildingHelper() {
        CustomGameEventManager.RegisterListener('gnv_request', (userID, keys) => this.SendGNV(keys));
        CustomGameEventManager.RegisterListener('building_helper_build_command', (userID, keys) => this.BuildCommand(keys));

        ListenToGameEvent('game_rules_state_change', event => this.OnGameRulesStateChange(event), this);
        ListenToGameEvent('npc_spawned', event => this.OnNPCSpawned(event), this);
    }

    /**
     * 发送网格
     */
    private SendGNV(args) {
        const playerID = args.PlayerID;
        const player = PlayerResource.GetPlayer(playerID);
        if (player != null) {
            this.print('Sending GNV to player ' + playerID);
            CustomGameEventManager.Send_ServerToPlayer(player, 'gnv_register', {
                gnv: this.Encoded,
                squareX: this.squareX,
                squareY: this.squareY,
                boundX: this.minBoundX,
                boundY: this.minBoundY,
            } as never);
        }
    }

    private print(message) {
        if (BuildingConfig.debugPrint) {
            print(message);
        }
    }

    private OnGameRulesStateChange(event: GameEventProvidedProperties & object) {
        const nowState = GameRules.State_Get();
        if (nowState == GameState.CUSTOM_GAME_SETUP) {
            this.__InitGNV();
        }
    }

    private OnNPCSpawned(event: NpcSpawnedEvent) {
        const npc = EntIndexToHScript(event.entindex) as CDOTA_BaseNPC;
        if (this.IsBuilder(npc)) {
            this.InitializeBuilder(npc);
        }
    }

    private IsBuilder(unit: CDOTA_BaseNPC) {
        const table = CustomNetTables.GetTableValue('builders', tostring(unit.GetEntityIndex()));
        return unit.GetUnitLabel() == 'builder' || (table && table['IsBuilder'] == 1) || unit.IsHero();
    }

    private InitializeBuilder(unit: CDOTA_BaseNPC) {
        if (unit.buildingQueue == null) {
            unit.buildingQueue = {};
        }
        CustomNetTables.SetTableValue('builders', tostring(unit.GetEntityIndex()), { IsBuilder: true });
    }

    private RemoveBuilder(unit: CDOTA_BaseNPC) {
        CustomNetTables.SetTableValue('builders', tostring(unit.GetEntityIndex()), { IsBuilder: false });
    }

    private BuildCommand(args: any) {
        const nPlayerID = args.PlayerID;
        const location = Vector(args.x, args.y, args.z);
        const queue = args.Queue == 1;
        const builder = EntIndexToHScript(args.builder) as CDOTA_BaseNPC;
        if (!queue) {
            builder.Stop();
        }
        this.AddToQueue(builder, location, queue);
    }

    private AddToQueue(builder: CDOTA_BaseNPC, location: Vector, bQueued: boolean) {
        const playerID = builder.GetMainControllingPlayer() as PlayerID;
        const player = PlayerResource.GetPlayer(playerID);
        const playerTable = this.GetPlayerTable(playerID);
        const buildingName = playerTable.activeBuilding;
        const buildingTable = playerTable.activeBuildingTable;
        // const fMaxScale = buildingTable.GetVal('MaxScale', 'float');
        // const size = buildingTable.GetVal('ConstructionSize', 'number');
        // const pathing_size = buildingTable.GetVal('BlockGridNavSize', 'number');
        const size = 2;
        const callbacks = playerTable.activeCallbacks;

        this.SnapToGrid(size, location);

        if (!this.ValidPosition(size, location, builder, callbacks)) return;

        const work = { location: location, name: buildingName, buildingTable: buildingTable, callbacks: callbacks };
        callbacks.onBuildingPosChosen(location);

        if (builder.GetUnitName() == buildingName) {
            this.ClearQueue(builder);
            table.insert(builder.buildingQueue, work);
            this.AdvanceQueue(builder as CDOTA_BaseNPC_Builder);
        } else {
        }
    }

    private ClearQueue(builder: CDOTA_BaseNPC) {
        let work = builder.work;
        builder.work = null;
        builder.state = 'idle';
        this.StopGhost(builder);

        if (builder.move_to_build_timer) {
            Timers.RemoveTimer(builder.move_to_build_timer);
        }

        if (!builder.buildingQueue || (!work && this.GetTableLength(builder.buildingQueue) == 0)) {
            return;
        }

        if (work) {
            this.ClearWorkParticles(work);
            if (work.entity) this.RemoveEntity(work.entity.prop);
            if (!work.inProgress && !work.isRepair) {
                work.refund = true;
            }
            if (work.name && work.callbacks && work.callbacks.onConstructionCancelled) work.callbacks.onConstructionCancelled(work);
        }

        for (let index = 0; index < this.GetTableLength(builder.buildingQueue); index++) {
            work = builder.buildingQueue[1];
            work.refund = true;
            this.ClearWorkParticles(work);
            if (work.entity) {
                this.RemoveEntity(work.entity.prop);
                this.RemoveEntity(work.entity);
            }
            table.remove(builder.buildingQueue, 1);
            if (work.name && work.callbacks && work.callbacks.onConstructionCancelled) work.callbacks.onConstructionCancelled(work);
        }
    }

    private AdvanceQueue(builder: CDOTA_BaseNPC_Builder) {
        if (builder.move_to_build_timer) {
            Timers.RemoveTimer(builder.move_to_build_timer);
        }

        if (builder.buildingQueue && this.GetTableLength(builder.buildingQueue) > 0) {
            const work = builder.buildingQueue[1];
            table.remove(builder.buildingQueue, 1);
            if (work.building) {
                if (!IsValidEntity(work.building) || !work.building.IsAlive()) {
                    this.AdvanceQueue(builder);
                } else {
                    const building = work.building;
                    const callbacks = work.callbacks;
                    let castRange = builder.GetFollowRange(building);
                    if (building.repair_distance) {
                        castRange = math.max(building.repair_distance, castRange);
                    }
                    builder.work = work;
                    builder.repair_target = building;
                    builder.state = 'moving_to_repair';
                    ExecuteOrderFromTable({
                        UnitIndex: builder.GetEntityIndex(),
                        OrderType: UnitOrder.MOVE_TO_TARGET,
                        TargetIndex: building.GetEntityIndex(),
                        Queue: false,
                    });
                    builder.move_to_build_timer = Timers.CreateTimer(() => {
                        if (!IsValidEntity(builder) || !builder.IsAlive()) {
                            return;
                        }
                        if (!IsValidEntity(building) || !building.IsAlive()) {
                            return;
                        }

                        const distance = building.GetAbsOrigin().__sub(builder.GetAbsOrigin()).Length();
                        if (distance > castRange) {
                            return 0.03;
                        } else {
                            builder.repairRange = castRange;
                            this.StartRepair(builder, building);
                            return;
                        }
                    });
                }
            }
        }
    }

    private StartRepair(builder, building) {}

    private RemoveEntity(ent) {
        if (ent && !ent.BHDUMMY) {
            UTIL_Remove(ent);
        }
    }

    private ClearWorkParticles(work) {
        if (work.particleIndex) ParticleManager.DestroyParticle(work.particleIndex, true);
        if (work.propParticleIndex) ParticleManager.DestroyParticle(work.propParticleIndex, true);
    }

    private StopGhost(builder) {
        const player = builder.etPlayerOwner();
        CustomGameEventManager.Send_ServerToPlayer(player, 'building_helper_end', {} as never);
    }

    private GetPlayerTable(playerID: PlayerID) {
        if (!this.Players[playerID]) {
            this.Players[playerID] = {};
        }
        return this.Players[playerID];
    }

    private ValidPosition(size: number, location: Vector, unit: CDOTA_BaseNPC, callbacks: any) {
        let bBlocked;
        const playerTable = this.GetPlayerTable(unit.GetMainControllingPlayer() as PlayerID);
        let buildingName = playerTable.activeBuilding;
        if (unit.work) {
            buildingName = unit.work.name;
        }

        // const buildingTable = buildingName && GetUnitKV(buildingName);
        // const requires = buildingTable && buildingTable['Requires'];
        // const prevents = buildingTable && buildingTable['Prevents'];

        // if (requires) {
        // }

        // if (prevents) {
        //     bBlocked = bBlocked || this.AreaMeetsCriteria(size, location, prevents, 'one');
        // }

        if (bBlocked) {
            if (callbacks.onConstructionFailed) {
                callbacks.onConstructionFailed();
            }
            return false;
        }

        /** 检查建造区域是否有敌人 */
        // local construction_radius = size * 64
        // local target_type = DOTA_UNIT_TARGET_HERO + DOTA_UNIT_TARGET_BASIC
        // local flags = DOTA_UNIT_TARGET_FLAG_MAGIC_IMMUNE_ENEMIES + DOTA_UNIT_TARGET_FLAG_FOW_VISIBLE + DOTA_UNIT_TARGET_FLAG_NO_INVIS
        // local enemies = FindUnitsInRadius(unit:GetTeamNumber(), location, nil, construction_radius, DOTA_UNIT_TARGET_TEAM_ENEMY, target_type, flags, FIND_ANY_ORDER, false)

        // for _,enemy in pairs(enemies) do
        //     local origin = enemy:GetAbsOrigin()
        //     if not IsCustomBuilding(enemy) and BuildingHelper:EnemyIsInsideBuildingArea(enemy:GetAbsOrigin(), location, size) then
        //         if callbacks.onConstructionFailed then
        //             callbacks.onConstructionFailed()
        //         end
        //         return false
        //     end
        // end

        return true;
    }

    private SnapToGrid(size: number, location: Vector) {
        if (size % 2 != 0) {
            location.x = this.SnapToGrid32(location.x);
            location.y = this.SnapToGrid32(location.y);
        } else {
            location.x = this.SnapToGrid64(location.x);
            location.y = this.SnapToGrid64(location.y);
        }
    }

    private SnapToGrid64(coord: number) {
        return 64 * math.floor(0.5 + coord / 64);
    }

    private SnapToGrid32(coord: number) {
        return 32 + 64 * math.floor(coord / 64);
    }

    private __InitGNV() {
        this.print('初始化网格');
        const worldMin = Vector(GetWorldMinX(), GetWorldMinY(), 0);
        const worldMax = Vector(GetWorldMaxX(), GetWorldMaxY(), 0);

        const boundX1 = GridNav.WorldToGridPosX(worldMin.x);
        const boundX2 = GridNav.WorldToGridPosX(worldMax.x);
        const boundY1 = GridNav.WorldToGridPosY(worldMin.y);
        const boundY2 = GridNav.WorldToGridPosY(worldMax.y);

        this.print(GetWorldMinX() + ' ' + GetWorldMaxX() + ' ' + GetWorldMinY() + ' ' + GetWorldMaxY());
        this.print(boundX1 + ' ' + boundX2 + ' ' + boundY1 + ' ' + boundY2);

        const gnv = {} as LuaTable;
        let blockedCount = 0;
        let unblockedCount = 0;
        const blocked_map_zones = Entities.FindAllByName('*bh_blocked');

        for (let x = boundX1; x < boundX2; x++) {
            this.print(x);
            let shift = 4;
            let byte = 0;
            this.Terrain[x] = {};
            for (let y = boundY1; y < boundY2; y++) {
                const gridX = GridNav.GridPosToWorldCenterX(x);
                const gridY = GridNav.GridPosToWorldCenterY(y);
                const position = Vector(gridX, gridY, 0);
                let blocked = false;
                /** 判断树的阻挡 半径30 */
                const bTreeBlocked = GridNav.IsNearbyTree(position, 30, true);
                /** 判断地形阻挡 */
                const bTerrainBlocked = !GridNav.IsTraversable(position) || GridNav.IsBlocked(position);

                blocked = bTerrainBlocked && bTreeBlocked;
                /** 没被阻挡情况下 判断定义的实体、trigger 阻挡 */
                if (!blocked) {
                    for (const key of Object.keys(blocked_map_zones)) {
                        const entity = blocked_map_zones[key];
                        const triggerBlocked = this.IsInsideEntityBounds(entity, position);
                        if (triggerBlocked) {
                            blocked = true;
                            break;
                        }
                    }
                }

                if (blocked) {
                    this.Terrain[x][y] = BuildGridType.BLOCKED;
                    byte += bit.lshift(2, shift);
                    blockedCount += 1;
                } else {
                    this.Terrain[x][y] = BuildGridType.BUILDABLE;
                    byte += bit.lshift(2, shift);
                    unblockedCount += 1;
                }

                shift -= 2;

                if (shift == -2) {
                    gnv[this.GetTableLength(gnv) + 1] = string.char(byte + 58);
                    shift = 4;
                    byte = 0;
                }
            }

            if (shift != 4) {
                gnv[this.GetTableLength(gnv) + 1] = string.char(byte + 58);
            }
        }
        const gnv_string = table.concat(gnv as any, '');
        let last = null;
        let count = 0;
        const gnvRLE = {};
        for (let i = 1; i < string.len(gnv_string); i++) {
            const c = gnv_string.substring(i - 1, i);
            if (last != null) {
                if (last == c) {
                    count++;
                } else {
                    gnvRLE[this.GetTableLength(gnvRLE) + 1] = tostring(count) + last;
                    last = c;
                    count = 1;
                }
            } else {
                last = c;
                count++;
            }
        }
        const gnvRLE_string = table.concat(gnvRLE as any, '');
        const squareX = boundX2 - boundX1 + 1;
        const squareY = boundY2 - boundY1 + 1;

        this.Grid = this.Terrain;
        this.Encoded = gnvRLE_string;
        this.squareX = squareX;
        this.squareY = squareY;
        this.minBoundX = boundX1;
        this.minBoundY = boundY1;
        this.AllTrees = Entities.FindAllByClassname('ent_dota_tree');
        this.print('初始化网格--完成！');
    }

    public AddBuilding(keys: { builder: CDOTA_BaseNPC }) {
        const callbacks = this.SetCallbacks(keys);
        const builder = keys.builder;
        const nPlayerID = builder.GetMainControllingPlayer() as PlayerID;
        const player = PlayerResource.GetPlayer(nPlayerID);

        const unitName = builder.GetUnitName();

        const size = 2;
        const fMaxScale = 1;

        const playerTable = this.GetPlayerTable(nPlayerID);
        playerTable.activeBuilder = builder;
        playerTable.activeBuilding = unitName;
        // playerTable.activeBuildingTable = buildingTable;
        playerTable.activeCallbacks = callbacks;

        const event: any = { state: 'active', size: size, scale: fMaxScale, builderIndex: builder.GetEntityIndex() };
        event.modelOffset = 0;

        const mgd = this.GetOrCreateDummy(unitName);
        event.entindex = mgd.GetEntityIndex();
        event.range = 600;

        CustomGameEventManager.Send_ServerToPlayer(player, 'building_helper_enable', event as never);
    }

    private GetOrCreateDummy(unitName: string): CDOTA_BaseNPC {
        if (this.Dummies[unitName] != null) {
            return this.Dummies[unitName];
        } else {
            const mgd = CreateUnitByName(unitName, Vector(0, 0, 0), false, null, null, DotaTeam.NOTEAM);
            mgd.AddEffects(EntityEffects.EF_NODRAW);
            mgd.AddNewModifier(mgd, null, 'modifier_out_of_world', {});
            this.Dummies[unitName] = mgd;
            mgd['BHDUMMY'] = true;
            return mgd;
        }
    }

    private SetCallbacks(keys: any) {
        const callbacks: any = {};
        keys.OnPreConstruction = callback => {
            callbacks.OnPreConstruction = callback;
        };
        keys.OnBuildingPosChosen = callback => {
            callbacks.OnBuildingPosChosen = callback;
        };
        keys.OnConstructionFailed = callback => {
            callbacks.OnConstructionFailed = callback;
        };
        keys.OnConstructionCancelled = callback => {
            callbacks.OnConstructionCancelled = callback;
        };
        keys.OnConstructionStarted = callback => {
            callbacks.OnConstructionStarted = callback;
        };
        keys.OnConstructionCompleted = callback => {
            callbacks.OnConstructionCompleted = callback;
        };
        keys.OnBelowHalfHealth = callback => {
            callbacks.OnBelowHalfHealth = callback;
        };
        keys.OnAboveHalfHealth = callback => {
            callbacks.OnAboveHalfHealth = callback;
        };
    }

    /** 判断实体是否阻挡地点 */
    private IsInsideEntityBounds(entity: CBaseEntity, location: Vector) {
        const origin = entity.GetAbsOrigin();
        const bounds = entity.GetBounds();
        const min = bounds.Mins;
        const max = bounds.Maxs;
        const X = location.x;
        const Y = location.y;
        const minX = min.x + origin.x;
        const minY = min.y + origin.y;
        const maxX = max.x + origin.x;
        const maxY = max.y + origin.y;
        const betweenX = X >= minX && X <= maxX;
        const betweenY = Y >= minY && Y <= maxY;
        return betweenX && betweenY;
    }

    private GetTableLength(tbl: LuaTable | {}): number {
        let result = 0;
        for (const key in tbl) {
            result += 1;
        }
        return result;
    }
}

const enum BuildGridType {
    'BLOCKED',
    'BUILDABLE',
}
declare global {
    var BuildingHelper: BuildingHelper;
}
declare interface CDOTA_BaseNPC_Builder extends CDOTA_BaseNPC {
    buildingQueue: any;
    work: any;
    state: string;
    move_to_build_timer: string;
    repairRange: number;
    repair_target: CDOTA_BaseNPC;
    GetFollowRange(target: CDOTA_BaseNPC): number;
}
declare const CDOTA_BaseNPC_Builder: DotaConstructor<CDOTA_BaseNPC_Builder>;
CDOTA_BaseNPC_Builder.GetFollowRange = function (target): number {
    return this.GetHullRadius() + target.GetHullRadius() + 100;
};
